<html><head><meta charset="utf-8"/><title>AJAX</title></head><body><h1>AJAX</h1><div class="x-wiki-content x-main-content">
 <p>
  AJAX不是JavaScript的规范，它只是一个哥们“发明”的缩写：Asynchronous JavaScript and XML，意思就是用JavaScript执行异步网络请求。
 </p>
 <p>
  如果仔细观察一个Form的提交，你就会发现，一旦用户点击“Submit”按钮，表单开始提交，浏览器就会刷新页面，然后在新页面里告诉你操作是成功了还是失败了。如果不幸由于网络太慢或者其他原因，就会得到一个404页面。
 </p>
 <p>
  这就是Web的运作原理：一次HTTP请求对应一个页面。
 </p>
 <p>
  如果要让用户留在当前页面中，同时发出新的HTTP请求，就必须用JavaScript发送这个新请求，接收到数据后，再用JavaScript更新页面，这样一来，用户就感觉自己仍然停留在当前页面，但是数据却可以不断地更新。
 </p>
 <p>
  最早大规模使用AJAX的就是Gmail，Gmail的页面在首次加载后，剩下的所有数据都依赖于AJAX来更新。
 </p>
 <p>
  用JavaScript写一个完整的AJAX代码并不复杂，但是需要注意：AJAX请求是异步执行的，也就是说，要通过回调函数获得响应。
 </p>
 <p>
  在现代浏览器上写AJAX主要依靠
  <code>
   XMLHttpRequest
  </code>
  对象：
 </p>
 <pre><code class="language-x-javascript">'use strict';
----
function success(text) {
    var textarea = document.getElementById('test-response-text');
    textarea.value = text;
}

function fail(code) {
    var textarea = document.getElementById('test-response-text');
    textarea.value = 'Error code: ' + code;
}

var request = new XMLHttpRequest(); // 新建XMLHttpRequest对象

request.onreadystatechange = function () { // 状态发生变化时，函数被回调
    if (request.readyState === 4) { // 成功完成
        // 判断响应结果:
        if (request.status === 200) {
            // 成功，通过responseText拿到响应的文本:
            return success(request.responseText);
        } else {
            // 失败，根据响应码判断失败原因:
            return fail(request.status);
        }
    } else {
        // HTTP请求还在继续...
    }
}

// 发送请求:
request.open('GET', '/api/categories');
request.send();

alert('请求已发送，请等待响应...');
</code></pre>
 <textarea id="test-response-text" rows="5" style="width: 90%; margin: 15px 0; resize: none;">响应结果：
</textarea>
 <p>
  对于低版本的IE，需要换一个
  <code>
   ActiveXObject
  </code>
  对象：
 </p>
 <pre><code class="language-x-javascript">'use strict';
----
function success(text) {
    var textarea = document.getElementById('test-ie-response-text');
    textarea.value = text;
}

function fail(code) {
    var textarea = document.getElementById('test-ie-response-text');
    textarea.value = 'Error code: ' + code;
}

var request = new ActiveXObject('Microsoft.XMLHTTP'); // 新建Microsoft.XMLHTTP对象

request.onreadystatechange = function () { // 状态发生变化时，函数被回调
    if (request.readyState === 4) { // 成功完成
        // 判断响应结果:
        if (request.status === 200) {
            // 成功，通过responseText拿到响应的文本:
            return success(request.responseText);
        } else {
            // 失败，根据响应码判断失败原因:
            return fail(request.status);
        }
    } else {
        // HTTP请求还在继续...
    }
}

// 发送请求:
request.open('GET', '/api/categories');
request.send();

alert('请求已发送，请等待响应...');
</code></pre>
 <textarea id="test-ie-response-text" rows="5" style="width: 90%; margin: 15px 0; resize: none;">IE响应结果：
</textarea>
 <p>
  如果你想把标准写法和IE写法混在一起，可以这么写：
 </p>
 <pre><code>var request;
if (window.XMLHttpRequest) {
    request = new XMLHttpRequest();
} else {
    request = new ActiveXObject('Microsoft.XMLHTTP');
}
</code></pre>
 <p>
  通过检测
  <code>
   window
  </code>
  对象是否有
  <code>
   XMLHttpRequest
  </code>
  属性来确定浏览器是否支持标准的
  <code>
   XMLHttpRequest
  </code>
  。注意，
  <em>
   不要
  </em>
  根据浏览器的
  <code>
   navigator.userAgent
  </code>
  来检测浏览器是否支持某个JavaScript特性，一是因为这个字符串本身可以伪造，二是通过IE版本判断JavaScript特性将非常复杂。
 </p>
 <p>
  当创建了
  <code>
   XMLHttpRequest
  </code>
  对象后，要先设置
  <code>
   onreadystatechange
  </code>
  的回调函数。在回调函数中，通常我们只需通过
  <code>
   readyState === 4
  </code>
  判断请求是否完成，如果已完成，再根据
  <code>
   status === 200
  </code>
  判断是否是一个成功的响应。
 </p>
 <p>
  <code>
   XMLHttpRequest
  </code>
  对象的
  <code>
   open()
  </code>
  方法有3个参数，第一个参数指定是
  <code>
   GET
  </code>
  还是
  <code>
   POST
  </code>
  ，第二个参数指定URL地址，第三个参数指定是否使用异步，默认是
  <code>
   true
  </code>
  ，所以不用写。
 </p>
 <p>
  <em>
   注意
  </em>
  ，千万不要把第三个参数指定为
  <code>
   false
  </code>
  ，否则浏览器将停止响应，直到AJAX请求完成。如果这个请求耗时10秒，那么10秒内你会发现浏览器处于“假死”状态。
 </p>
 <p>
  最后调用
  <code>
   send()
  </code>
  方法才真正发送请求。
  <code>
   GET
  </code>
  请求不需要参数，
  <code>
   POST
  </code>
  请求需要把body部分以字符串或者
  <code>
   FormData
  </code>
  对象传进去。
 </p>
 <h3>
  安全限制
 </h3>
 <p>
  上面代码的URL使用的是相对路径。如果你把它改为
  <code>
   'http://www.sina.com.cn/'
  </code>
  ，再运行，肯定报错。在Chrome的控制台里，还可以看到错误信息。
 </p>
 <p>
  这是因为浏览器的同源策略导致的。默认情况下，JavaScript在发送AJAX请求时，URL的域名必须和当前页面完全一致。
 </p>
 <p>
  完全一致的意思是，域名要相同（
  <code>
   www.example.com
  </code>
  和
  <code>
   example.com
  </code>
  不同），协议要相同（
  <code>
   http
  </code>
  和
  <code>
   https
  </code>
  不同），端口号要相同（默认是
  <code>
   :80
  </code>
  端口，它和
  <code>
   :8080
  </code>
  就不同）。有的浏览器口子松一点，允许端口不同，大多数浏览器都会严格遵守这个限制。
 </p>
 <p>
  那是不是用JavaScript无法请求外域（就是其他网站）的URL了呢？方法还是有的，大概有这么几种：
 </p>
 <p>
  一是通过Flash插件发送HTTP请求，这种方式可以绕过浏览器的安全限制，但必须安装Flash，并且跟Flash交互。不过Flash用起来麻烦，而且现在用得也越来越少了。
 </p>
 <p>
  二是通过在同源域名下架设一个代理服务器来转发，JavaScript负责把请求发送到代理服务器：
 </p>
 <pre><code>'/proxy?url=http://www.sina.com.cn'
</code></pre>
 <p>
  代理服务器再把结果返回，这样就遵守了浏览器的同源策略。这种方式麻烦之处在于需要服务器端额外做开发。
 </p>
 <p>
  第三种方式称为JSONP，它有个限制，只能用GET请求，并且要求返回JavaScript。这种方式跨域实际上是利用了浏览器允许跨域引用JavaScript资源：
 </p>
 <pre><code>&lt;html&gt;
&lt;head&gt;
    &lt;script src="http://example.com/abc.js"&gt;&lt;/script&gt;
    ...
&lt;/head&gt;
&lt;body&gt;
...
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
 <p>
  JSONP通常以函数调用的形式返回，例如，返回JavaScript内容如下：
 </p>
 <pre><code>foo('data');
</code></pre>
 <p>
  这样一来，我们如果在页面中先准备好
  <code>
   foo()
  </code>
  函数，然后给页面动态加一个
  <code>
   &lt;script&gt;
  </code>
  节点，相当于动态读取外域的JavaScript资源，最后就等着接收回调了。
 </p>
 <p>
  以163的股票查询URL为例，对于URL：
  <a href="http://api.money.126.net/data/feed/0000001,1399001?callback=refreshPrice">
   http://api.money.126.net/data/feed/0000001,1399001?callback=refreshPrice
  </a>
  ，你将得到如下返回：
 </p>
 <pre><code>refreshPrice({"0000001":{"code": "0000001", ... });
</code></pre>
 <p>
  因此我们需要首先在页面中准备好回调函数：
 </p>
 <script>
  function refreshPrice(data) {
    var p = document.getElementById('test-jsonp');
    p.innerHTML = '当前价格：' +
        data['0000001'].name +': ' + 
        data['0000001'].price + '；' +
        data['1399001'].name + ': ' +
        data['1399001'].price;
}
function getPrice() {
    var
        js = document.createElement('script'),
        head = document.getElementsByTagName('head')[0];
    js.src = 'http://api.money.126.net/data/feed/0000001,1399001?callback=refreshPrice';
    head.appendChild(js);
}
 </script>
 <pre><code>function refreshPrice(data) {
    var p = document.getElementById('test-jsonp');
    p.innerHTML = '当前价格：' +
        data['0000001'].name +': ' + 
        data['0000001'].price + '；' +
        data['1399001'].name + ': ' +
        data['1399001'].price;
}
</code></pre>
 <div>
  <p id="test-jsonp">
   当前价格：
  </p>
  <p>
   <button onclick="getPrice()" type="button">
    刷新
   </button>
  </p>
 </div>
 <p>
  最后用
  <code>
   getPrice()
  </code>
  函数触发：
 </p>
 <pre><code>function getPrice() {
    var
        js = document.createElement('script'),
        head = document.getElementsByTagName('head')[0];
    js.src = 'http://api.money.126.net/data/feed/0000001,1399001?callback=refreshPrice';
    head.appendChild(js);
}
</code></pre>
 <p>
  就完成了跨域加载数据。
 </p>
 <h3>
  CORS
 </h3>
 <p>
  如果浏览器支持HTML5，那么就可以一劳永逸地使用新的跨域策略：CORS了。
 </p>
 <p>
  CORS全称Cross-Origin Resource Sharing，是HTML5规范定义的如何跨域访问资源。
 </p>
 <p>
  了解CORS前，我们先搞明白概念：
 </p>
 <p>
  Origin表示本域，也就是浏览器当前页面的域。当JavaScript向外域（如sina.com）发起请求后，浏览器收到响应后，首先检查
  <code>
   Access-Control-Allow-Origin
  </code>
  是否包含本域，如果是，则此次跨域请求成功，如果不是，则请求失败，JavaScript将无法获取到响应的任何数据。
 </p>
 <p>
  用一个图来表示就是：
 </p>
 <p>
  <img alt="js-cors" data-src="/files/attachments/1027024093709472/l" src="https://www.liaoxuefeng.com/files/attachments/1027024093709472/l"/>
 </p>
 <p>
  假设本域是
  <code>
   my.com
  </code>
  ，外域是
  <code>
   sina.com
  </code>
  ，只要响应头
  <code>
   Access-Control-Allow-Origin
  </code>
  为
  <code>
   http://my.com
  </code>
  ，或者是
  <code>
   *
  </code>
  ，本次请求就可以成功。
 </p>
 <p>
  可见，跨域能否成功，取决于对方服务器是否愿意给你设置一个正确的
  <code>
   Access-Control-Allow-Origin
  </code>
  ，决定权始终在对方手中。
 </p>
 <p>
  上面这种跨域请求，称之为“简单请求”。简单请求包括GET、HEAD和POST（POST的Content-Type类型
仅限
  <code>
   application/x-www-form-urlencoded
  </code>
  、
  <code>
   multipart/form-data
  </code>
  和
  <code>
   text/plain
  </code>
  ），并且不能出现任何自定义头（例如，
  <code>
   X-Custom: 12345
  </code>
  ），通常能满足90%的需求。
 </p>
 <p>
  无论你是否需要用JavaScript通过CORS跨域请求资源，你都要了解CORS的原理。最新的浏览器全面支持HTML5。在引用外域资源时，除了JavaScript和CSS外，都要验证CORS。例如，当你引用了某个第三方CDN上的字体文件时：
 </p>
 <pre><code>/* CSS */
@font-face {
  font-family: 'FontAwesome';
  src: url('http://cdn.com/fonts/fontawesome.ttf') format('truetype');
}
</code></pre>
 <p>
  如果该CDN服务商未正确设置
  <code>
   Access-Control-Allow-Origin
  </code>
  ，那么浏览器无法加载字体资源。
 </p>
 <p>
  对于PUT、DELETE以及其他类型如
  <code>
   application/json
  </code>
  的POST请求，在发送AJAX请求之前，浏览器会先发送一个
  <code>
   OPTIONS
  </code>
  请求（称为preflighted请求）到这个URL上，询问目标服务器是否接受：
 </p>
 <pre><code>OPTIONS /path/to/resource HTTP/1.1
Host: bar.com
Origin: http://my.com
Access-Control-Request-Method: POST
</code></pre>
 <p>
  服务器必须响应并明确指出允许的Method：
 </p>
 <pre><code>HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://my.com
Access-Control-Allow-Methods: POST, GET, PUT, OPTIONS
Access-Control-Max-Age: 86400
</code></pre>
 <p>
  浏览器确认服务器响应的
  <code>
   Access-Control-Allow-Methods
  </code>
  头确实包含将要发送的AJAX请求的Method，才会继续发送AJAX，否则，抛出一个错误。
 </p>
 <p>
  由于以
  <code>
   POST
  </code>
  、
  <code>
   PUT
  </code>
  方式传送JSON格式的数据在REST中很常见，所以要跨域正确处理
  <code>
   POST
  </code>
  和
  <code>
   PUT
  </code>
  请求，服务器端必须正确响应
  <code>
   OPTIONS
  </code>
  请求。
 </p>
 <p>
  需要深入了解CORS的童鞋请移步
  <a href="http://www.w3.org/TR/cors/">
   W3C文档
  </a>
  。
 </p>
</div>
</body></html>